#!/usr/bin/perl
################################################################################
# Converts an iTunes XML library file into an index of poorly-specified YAML
# and a set of XSPF playlists.
#
# Released under the terms of the MIT license 2006 by Darren Kulp
# http://code.google.com/p/sift/source
################################################################################
package Mac::iTunes::Library::Parse::ToPlaylists;

use strict;
use warnings;

use HTML::Entities::Numbered;
use Mac::PropertyList;
use Mac::PropertyList::SAX qw(parse_plist_file);
use URI::Escape;
use XML::XSPF;
use XML::XSPF::Track;
use YAML::Syck qw(Load Dump);

sub curse ($);

my $data = parse_plist_file(shift);

################################################################################
# Configuration variables to change
my $creator = "Your name here";
my %pdir = (
	id		=> "id",
	name	=> "name",
);
my $datadir = ".";
my (@lists, %tracks, %pers);
my @skip = qw(Videos);
################################################################################

# TODO: Remove unnecessary printing business or make it optional
printf "%20s %5s\n", "Name", "Items";
for my $playlist (@{ $data->{Playlists} }) {
	my $items = $playlist->{'Playlist Items'} || [ ];
	printf "%20s %5s\n", $playlist->{Name}, scalar @$items;
	push @lists, {
		((map { $_ => curse($playlist->{$_}) }
				grep { !/^Playlist Items$/ } keys %$playlist),
		("Playlist Items" => [
			map {
				($tracks{$_->{'Track ID'}} ||=
					curse($data->{Tracks}{$_->{'Track ID'}}))
			} @$items
		]))
	};
}

delete @{$data}{qw(Playlists Tracks)};
$data = curse($data);
$data->{Playlists} = \@lists;
$data->{Tracks} = \%tracks;

my %struct;	# Tracks playlist hierarchy
my %children;	# Temporarily tracks children

for my $list (@{ $data->{Playlists} }) {
	next if grep { $_ eq $list->{'Name'} } @skip;
	$struct{$list->{'Playlist Persistent ID'}} = {
		id			=> $list->{'Playlist ID'},
		isFolder	=> $list->{'Folder'} ? 1 : 0,
		name		=> $list->{'Name'},
		pid			=> $list->{'Playlist Persistent ID'},
		smart		=> $list->{'Smart Info'} ? 1 : 0,
		special		=> (grep { $list->{$_} }
			qw(Master Party Shuffle Podcasts Videos))[0] || undef 
	};
	if (my $parent = $list->{'Parent Persistent ID'}) {
		push @{ $children{$parent} }, $list->{'Playlist Persistent ID'};
	}

	my $xl = new XML::XSPF;

	$xl->creator($creator);
	$xl->title($list->{Name});
	$xl->trackList(
		map {
			my $xt = new XML::XSPF::Track;

			$xt->album($_->{Album});
			$xt->annotation($_->{Comments});
			$xt->creator($_->{Artist});
			$xt->duration($_->{'Total Time'});
			$xt->location($_->{Location});
			$xt->title($_->{Name});

			$xt
		} @{ $list->{'Playlist Items'} }
	);

	# TODO: trap errors in symlink
	my $file = "$pdir{id}/$list->{'Playlist Persistent ID'}";
	open my($fh), ">", $file;
	print $fh name2decimal_xml($xl->toString);
	close $fh;
	# TODO: more explanation and/or configurability here; we can't really know
	# that one '..' will work
	symlink "../$file", "$pdir{name}/" . uri_escape($list->{Name}) . ".xspf";
}
for my $pid (keys %children) {
	$struct{$pid}{children} = $children{$pid};
}
# Here's the poorly-specified index. FIXME: Rework into some kind of XML ?
open my($fh), ">", "$datadir/playlist_hierarchy";
print $fh Dump \%struct;
close $fh;

# Recursively call stringification overloads to remove object nature
sub curse ($) {
	my $data = shift;
	return $data unless ref($data);
	if (UNIVERSAL::isa($data, 'ARRAY')) {
		$data = [ map { curse($_) } @$data ];
	} elsif (UNIVERSAL::isa($data, 'HASH')) {
		$data = { map { $_ => curse($data->{$_}) } keys %$data };
	} else {
		$data = "$data";
	}
	$data
}

# Add stringification overload for easier coding
package Mac::PropertyList::Scalar;
use overload '""' => sub { shift->as_basic_data };
package Mac::PropertyList::Boolean;
use overload '""' => sub { shift->value };

1;
__END__

TODO: Documentation!
